package wsi.resolve;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;

import org.rygh.core.html.Html;
import org.rygh.core.html.element.HtmlAnchor;
import org.rygh.core.html.element.HtmlHr;
import org.rygh.core.html.element.HtmlOl;
import org.rygh.core.html.element.HtmlParagraph;
import org.rygh.core.html.element.HtmlQuote;
import org.rygh.core.html.element.HtmlWrapper;
import org.rygh.core.html.table.HtmlTable;
import org.rygh.core.html.table.TableData;
import org.rygh.core.html.table.TableTuple;
import org.rygh.core.model.common.MapList;
import org.rygh.core.util.UIo;
import org.rygh.core.util.UMath;
import org.rygh.core.util.counter.CounterTuple;
import org.rygh.core.util.counter.FrequencyCounter;
import org.rygh.core.util.counter.SimpleCounter;
import org.rygh.semantic.gui.swing.input.Input;
import org.rygh.semantic.model.ex.PredicatePath;

import wsi.dataguide.tree.WSXmlLeaf;
import wsi.function.DirManager;
import wsi.function.Function;
import wsi.function.Function.Phrase;
import wsi.match.MatchProperty;
import wsi.match.MatchProperty.MatchType;
import wsi.match.MatchProperty.Threshold;
import wsi.match.SubtreeMatch;
import wsi.script.AppXml;
import wsi.subgraph.entity.EntityGroup;
import wsi.subgraph.entity.EntityGroupController;
import wsi.subgraph.entity.EntityResolve;
import wsi.subgraph.entity.ResolvePair;
import wsi.subgraph.tree.EntityLeaf;

public class Aggregator {
	protected final Logger log = Logger.getLogger(getClass().getName());

	private static final Map<Function, String> matchPropertyMap = new HashMap<>();

	public static HtmlTable toMatchInfoTable() {
		TableData data = new TableData();
		TableTuple tuple = new TableTuple();

		tuple.add("Service", "Function", "Log");
		data.add(tuple);

		for (Entry<Function, String> e : matchPropertyMap.entrySet()) {
			tuple = new TableTuple();
			tuple.add(e.getKey().getService(), e.getKey().getName(),
					e.getValue());

			data.add(tuple);
		}

		data.sort();

		return new HtmlTable("Match Property Log", data);
	}

	private final Map<Input, EntityResolve> entityResolveMap = new HashMap<>();

	// map from subroot (xml path) to harmonic mean, showing e_i=t_j
	private final Map<String, Double> inputEntityResolveMap = new HashMap<>();

	// map from predicate path to harmonic mean of maximal prob of all, showing
	// likelihood
	private final Map<String, Double> entityGroupResolveMapMaxAll1 = new HashMap<>();

	// map from predicate path to harmonic mean of maximal prob of one
	private final Map<String, Double> entityGroupResolveMapMaxOne2 = new HashMap<>();

	// map from predicate path to harmonic mean of maximal prob with opposite
	// match of all
	private final Map<String, Double> entityGroupResolveMapMaxAllOpposite3 = new HashMap<>();

	// map from predicate path to harmonic mean of maximal prob with opposite
	// match of one
	private final Map<String, Double> entityGroupResolveMapMaxOneOpposite4 = new HashMap<>();

	private final SimpleCounter<String> oppositeMatchCounter = new SimpleCounter<>();
	private final FrequencyCounter<Input> oppositeMatchTreewiseCounter = new FrequencyCounter<>();

	private final Input[] inputs;
	private final DirManager dir;

	private final Function function;
	private Map<WSXmlLeaf, SubtreeMatch> matchMap;

	private MapList<Input, WSXmlLeaf> subrootMapList;

	private MatchProperty matchProperty;
	private int subtreeCount;

	public Aggregator(DirManager dir) {
		this.inputs = dir.getInputs();

		this.dir = dir;
		function = dir.getFunction();
	}

	/**
	 * for input entity, try exists-match approach, for the rest group, try
	 * forall-match approach.
	 * 
	 * @param allMaxPairList
	 */
	public void aggregate(MapList<Input, ResolvePair> oneMaxPairList,
			MapList<Input, ResolvePair> allMaxPairList) {
		// System.out.println("\t\t aggregating");

		// for input entity.

		EntityGroupController egroupCtrl;
		EntityResolve eresolve;
		EntityLeaf enode;
		String key;

		List<ResolvePair> pairs;
		MapList<String, Double> doubleMapList = new MapList<>();

		for (Input input : inputs) {
			// log.info(String.format("aggregate input %s", input));

			if (!entityResolveMap.containsKey(input)) {
				// log.info(String
				// .format("No resolution info for input %s", input));
				continue;
			}

			eresolve = entityResolveMap.get(input);
			egroupCtrl = eresolve.getEntityGroupController();

			enode = egroupCtrl.getInputEntity();
			pairs = eresolve.getResolvePairs(enode);

			if (pairs == null) {
				log.info(String.format(
						"No entity resolve info for input node %s", enode
								.getValue().getValue()));
				continue;
			}

			for (ResolvePair p : pairs) {
				key = p.getXmlLeaf().getXpath().getValue();
				doubleMapList.put(key, p.getProbability());
			}

			if (function.isSkipped(Phrase.REST)) {
				log.info("break for trial.");
				break;
			}
		}

		if (AppXml.__DEBUG)
			UIo.saveFile(new File(dir.getDebugDir(),
					"_debug_input_entity_resolve_meta.txt"), doubleMapList
					.toString());

		for (Entry<String, List<Double>> entry : doubleMapList.entrySet()) {
			// System.out.printf("inputEntityResolveMap %s: %.4f, %s\n",
			// entry.getKey(), UMath.getHarmonicMean(entry.getValue()),
			// entry.getValue());

			inputEntityResolveMap.put(entry.getKey(),
					UMath.getAverageSpecial(entry.getValue()));
		}

		// for each entity group, mean of max for all

		doStatistics(entityGroupResolveMapMaxAll1, true, false);
		doStatistics(entityGroupResolveMapMaxOne2, false, false);
		doStatistics(entityGroupResolveMapMaxAllOpposite3, true, true, null,
				null, allMaxPairList);
		doStatistics(entityGroupResolveMapMaxOneOpposite4, false, true,
				oppositeMatchCounter, oppositeMatchTreewiseCounter,
				oneMaxPairList);
	}

	/**
	 * a very important function!
	 * 
	 * @return
	 */
	public MatchProperty computeMatch() {
		log.info("compute match for " + function.getFullName());
		// by default
		matchProperty = new MatchProperty(MatchType.EXISTS);
		matchProperty.setSubtreeIndex(1);

		String info, xpath = null;
		double d, max_input_prob = -1;
		int i_2, i_5, i_8, i_9;
		i_2 = i_5 = i_8 = i_9 = 0;

		int subtree_count = inputEntityResolveMap.size();

		for (Entry<String, Double> i : inputEntityResolveMap.entrySet()) {
			d = i.getValue();

			if (d > Threshold.MINORITY)
				i_2++;

			if (d > Threshold.MIDDLE)
				i_5++;

			if (d > Threshold.MAJORITY)
				i_8++;

			if (d > Threshold.PERFECT_MATCH)
				i_9++;

			if (max_input_prob < d) {
				max_input_prob = d;
				xpath = i.getKey();
			}
		}

		boolean matched = false;

		double d_2 = 1. * i_2 / subtree_count;
		double d_5 = 1. * i_5 / subtree_count;
		double d_8 = 1. * i_8 / subtree_count;

		/*
		 * we start from certain cases
		 */

		List<CounterTuple<String>> oppositeMatchRankedList = null;

		if (!oppositeMatchCounter.isEmpty())
			oppositeMatchRankedList = oppositeMatchCounter.getListSortDesc();

		StringBuilder sbuilder = new StringBuilder();

		if (!matched) {
			if (inputEntityResolveMap.size() == 1) {
				// exists, if there is only one subtree

				if (i_9 == 0) {
					info = "[warn] no best match, relegated.";
					sbuilder.append(info + "<br>");
					log.info(info);
				}

				info = String.format("EXISTS, only one subtree @ %s", xpath);
				sbuilder.append(info + "<br>");
				log.info(info);

				matchProperty = new MatchProperty(MatchType.EXISTS);
				matchProperty.setSubtreeIndex(1);
				matched = true;
			}
		}

		if (!matched) {
			if (i_9 == 1 && d_5 < Threshold.MIDDLE) {
				/*
				 * exists, one best. it implies max_input_prob > 0.9
				 */

				info = String.format("EXISTS, one best @ %s", xpath);
				sbuilder.append(info + "<br>");
				log.info(info);

				matchProperty = new MatchProperty(MatchType.EXISTS);
				matchProperty.setSubtreeIndex(1);
				matched = true;
			}
		}

		if (!matched) {
			/*
			 * it may be exists-match for this case.
			 */

			double opposite_ratio = Double.NaN;
			int count = 0;

			if (d_2 > Threshold.MIDDLE) {
				if (oppositeMatchRankedList != null) {
					count = oppositeMatchRankedList.get(0).getCount();

					opposite_ratio = 1. * count / subtreeCount;

					if (opposite_ratio > Threshold.MINORITY) {
						info = String
								.format("FORALL, ratio d_2 %.2f, d_5 %.2f, d_8 %.2f, "
										+ "opposite ratio %.2f (%d / %d), subtree @ %s",
										d_2, d_5, d_8, opposite_ratio, count,
										subtreeCount, xpath);
						sbuilder.append(info + "<br>");
						log.info(info);

						matchProperty = getForallMatch(sbuilder);
						matched = true;
					}
				}
			}

			if (!matched) {
				info = String
						.format("NOT FORALL, ratio d_2 %.2f, d_5 %.2f, d_8 %.2f, "
								+ "opposite ratio %.2f (%d / %d), subtree @ %s",
								d_2, d_5, d_8, opposite_ratio, count,
								subtreeCount, xpath);
				sbuilder.append(info + "<br>");
				log.info(info);
			}
		}

		/*
		 * till now, if still not match, we focus on whether it can be matched
		 * as forall (as exists is by default), instead of whether it can be
		 * matched as exists.
		 */

		if (!matched) {
			// try forall-match
			if (oppositeMatchRankedList != null) {

			}
		}

		// as the default is exists@1, thus the following is no longer needed.
		//
		// if (!matched) {
		// if (max_input_prob > Threshold.PERFECT_MATCH ||
		// /**/(less_ratio > Threshold.MINORITY
		// /**/&& max_input_prob > Threshold.MIDDLE)) {
		//
		// if (less_ratio > Threshold.MINORITY) {
		// info = String.format(
		// "EXISTS, less ratio > minor, subtree @ %s", xpath);
		// sbuilder.append(info + "<br>");
		// log.info(info);
		//
		// XmlPath xmlPath = new XmlPath(xpath);
		// property = new MatchProperty(MatchType.EXISTS);
		// property.setSubtreeIndex(xmlPath.getLastIndex());
		// property.setSubtreeIndex(1);
		// matched = true;
		// }
		// }
		// }

		if (!matched) {
			info = String.format(
					"BY DEFAULT, cannot detect. max input prob. %.4f, "
							+ "opposite is empty? %b, subtree %d, "
							+ "ratio d_2 %.2f, d_5 %.2f, d_8 %.2f, ",
					max_input_prob, oppositeMatchCounter.isEmpty(),
					subtreeCount, d_2, d_5, d_8);
			sbuilder.append(info + "<br>");
			log.info(info);
		}

		matchPropertyMap.put(function, sbuilder.toString());
		return matchProperty;
	}

	private void doStatistics(Map<String, Double> map, boolean forall,
			boolean opposite) {
		doStatistics(map, forall, opposite, null, null, null);
	}

	private void doStatistics(Map<String, Double> map, boolean forall,
			boolean opposite, SimpleCounter<String> oppoMatchCounter,
			FrequencyCounter<Input> pairwise,
			MapList<Input, ResolvePair> pairMapList) {

		MapList<String, Double> doubleMapList = new MapList<>();
		EntityGroupController egroupCtrl;
		EntityResolve eresolve;

		ResolvePair pair, pair_mark;
		PredicatePath pred_path;

		String key;
		double prob;

		List<WSXmlLeaf> subroots;
		List<EntityGroup> egroups;
		subtreeCount = 0;

		for (Input input : inputs) {
			if (!entityResolveMap.containsKey(input)) {
				// log.info(String
				// .format("No resolution info for input %s", input));
				continue;
			}

			subroots = subrootMapList.get(input);
			eresolve = entityResolveMap.get(input);
			egroupCtrl = eresolve.getEntityGroupController();

			/*
			 * there is no need to check for input, starting from d = 1
			 */

			for (int d = 1; d < egroupCtrl.getDistance(); d++) {
				egroups = egroupCtrl.getGroups(d);

				for (EntityGroup group : egroups) {
					key = group.getKey();
					pred_path = group.getPredicatePath();
					prob = -1;
					pair_mark = null;

					// System.out.printf(
					// "aggregate group %s, forall %b, opposite %b\n",
					// key, forall, opposite);

					for (WSXmlLeaf subroot : subroots) {
						if (!matchMap.containsKey(subroot))
							continue;

						if (opposite)
							pair = eresolve.getPairOfMaxProbWithOppositeMatch(
									subroot, group, matchMap.get(subroot));
						else
							pair = eresolve.getPairOfMaxProb(subroot, group);

						// w:type::
						if (pair == null) {
							// System.out.printf("pair == null for %s\n",
							// subroot.getXpath());
							continue;
						}

						if (oppoMatchCounter != null)
							oppoMatchCounter.count(key, pred_path);

						if (pairwise != null) {
							// System.out
							// .printf("Input %s, Key %s, (d=%d) pair with max prob. %s\n",
							// input, key, d, pair);
							pairwise.count(input, pred_path, 1, subroots.size());
						}

						if (forall) {
							doubleMapList.put(key, pair.getProbability());

							if (pairMapList != null) {
								pairMapList.put(input, pair);

								// System.out.printf(
								// "allMax pair input %s, pair %s\n",
								// input, pair);
							}
						} else {
							if (prob < pair.getProbability()) {
								prob = pair.getProbability();

								pair_mark = pair;
							}
						}
					}

					if (!forall && prob > 0) {
						doubleMapList.put(key, prob);

						if (pairwise != null) {
							// System.out.printf(
							// "\t-> (d=%d) pair with max prob. %s\n\n",
							// d, pair_mark);
						}

						if (pairMapList != null)
							pairMapList.put(input, pair_mark);
					}
				}
			}

			if (function.isSkipped(Phrase.REST)) {
				log.info("break for trial.");
				break;
			}
		}

		if (pairwise != null) {
			/*
			 * for some subtrees of some inputs, there is no match. for these
			 * subtrees, we do not count as valid subtrees.
			 */
			subtreeCount = pairwise.getTotalSize();
		}

		for (Entry<String, List<Double>> entry : doubleMapList.entrySet())
			map.put(entry.getKey(), UMath.getAverageSpecial(entry.getValue()));
	}

	public Map<Input, EntityResolve> getEntityResolveMap() {
		return entityResolveMap;
	}

	private MatchProperty getForallMatch(StringBuilder sbuilder) {
		assert (!oppositeMatchCounter.isEmpty());

		List<CounterTuple<String>> oppositeMatchRankedList = oppositeMatchCounter
				.getListSortDesc();

		String info, theKey, maxKey;

		maxKey = oppositeMatchRankedList.get(0).getKey();
		theKey = maxKey; // as default value.

		int maxOppositeMatchCount = oppositeMatchCounter.getMaxCount();
		double opposite_max_mean = entityGroupResolveMapMaxOneOpposite4
				.get(maxKey);

		/*
		 * 131016 opposite_max_mean is not a strong criterion, abandon.
		 */

		// if (opposite_max_mean > Threshold.MAJORITY) {

		/*
		 * for y:created-::y:created:: and y:created-::, we will choose the one
		 * which is longer.
		 * 
		 * we do not choose INPUT here.
		 */

		String longer_key = null;
		int max_count = 0, count;

		for (String k : oppositeMatchCounter.keySet()) {
			if (k == maxKey || !k.startsWith(maxKey))
				continue;

			count = oppositeMatchCounter.get(k);

			if (max_count < count) {
				longer_key = k;
				max_count = count;
			}
		}

		if (longer_key != null) {
			theKey = longer_key;

			double t = Threshold.MAJORITY;

			if (entityGroupResolveMapMaxOneOpposite4.get(longer_key) < t) {
				info = String
						.format("[warn] longer key %s prob. should be larger than %.2f",
								longer_key, t);
				sbuilder.append(info + "<br>");
				log.info(info);
			}
		}

		info = String.format("FORALL @ %s, oppo max mean %.4f, "
				+ "opposite match %d", theKey, opposite_max_mean,
				maxOppositeMatchCount);
		sbuilder.append(info + "<br>");
		log.info(info);

		PredicatePath pred_path = new PredicatePath(theKey);
		MatchProperty property = new MatchProperty(MatchType.FORALL);
		property.setPredicatePath(pred_path);

		return property;
	}

	private HtmlTable getMapTable(Map<String, Double> map, String... strings) {
		TableData data = new TableData();
		TableTuple tuple = new TableTuple();

		tuple.add(strings[1], strings[2]);
		data.add(tuple);

		for (Entry<String, Double> e : map.entrySet()) {
			tuple = new TableTuple();
			tuple.add(e.getKey());
			tuple.add(String.format("%.4f", e.getValue()));

			data.add(tuple);
		}

		data.sort(1);

		return new HtmlTable(strings[0], data, false);
	}

	public MatchProperty getMatchProperty() {
		return matchProperty;
	}

	public int getSubtreeCount() {
		return subtreeCount;
	}

	public void setMatchMap(Map<WSXmlLeaf, SubtreeMatch> matchMap) {
		this.matchMap = matchMap;
	}

	public void setSubrootMapList(MapList<Input, WSXmlLeaf> subrootMapList) {
		this.subrootMapList = subrootMapList;
	}

	public String toMetaString() {
		Html html = new Html(dir.getServiceName() + "/" + function.getName());

		HtmlAnchor anchor;
		HtmlOl ol = new HtmlOl();

		Map<Input, HtmlAnchor> anchorMap = new HashMap<>();

		for (Input e : inputs) {
			anchor = new HtmlAnchor(e.getValue());

			ol.addElement(anchor);
			anchorMap.put(e, anchor);
		}

		html.addElement(ol);
		html.newline();

		/**/

		EntityResolve eresolve;

		for (Input input : inputs) {
			if (!entityResolveMap.containsKey(input)) {
				// log.info(String
				// .format("No resolution info for input %s", input));
				continue;
			}

			html.addElement(new HtmlWrapper(anchorMap.get(input)
					.toAnchorString()));

			eresolve = entityResolveMap.get(input);
			html.addElement(eresolve.getEntityGroupController().toHtmlTable());
			html.newline();

			html.addElement(eresolve.toHtmlTable());
			html.newline();
			html.addElement(new HtmlHr());

			if (function.isSkipped(Phrase.REST)) {
				log.info("break for trial.");
				break;
			}
		}

		return html.toString();
	}

	public String toStatisticsString() {
		Html html = new Html(dir.getServiceName() + "/" + function.getName());

		html.addElement(new HtmlParagraph("Subtree count: " + subtreeCount));
		html.newline();

		html.addElement(matchProperty.toHtmlElement());
		html.addElement(new HtmlQuote(matchPropertyMap.get(function)));
		html.newline();

		html.addElement(getMapTable(inputEntityResolveMap,
				"Input Entity Resolution", "Subtree",
				"Harmonic Mean of Resolution Probability"));
		html.newhr();

		html.addElement(getMapTable(
				entityGroupResolveMapMaxOneOpposite4,
				"Entity Group Resolution Fourth Edition",
				"Predicate Path",
				"Harmonic Mean of Maximal Resolution Probability For One Max With Opposite Match"));
		html.newline();
		html.addElement(new HtmlParagraph("Opposite Match Counter"));
		html.addElement(oppositeMatchCounter.toHtmlTable());
		html.newline();
		html.addElement(new HtmlParagraph("Opposite Match Tree-wise Counter"));
		html.addElement(oppositeMatchTreewiseCounter.toHtmlTable());
		html.newhr();

		html.addElement(getMapTable(entityGroupResolveMapMaxAll1,
				"Entity Group Resolution", "Predicate Path",
				"Harmonic Mean of Maximal Resolution Probability For All Max"));
		html.newline();

		html.addElement(getMapTable(entityGroupResolveMapMaxOne2,
				"Entity Group Resolution Second Edition", "Predicate Path",
				"Harmonic Mean of Maximal Resolution Probability For One Max"));
		html.newline();

		html.addElement(getMapTable(
				entityGroupResolveMapMaxAllOpposite3,
				"Entity Group Resolution Third Edition",
				"Predicate Path",
				"Harmonic Mean of Maximal Resolution Probability For All Max With Opposite Match"));
		html.newline();

		return html.toString();
	}
}
